package dedup

import (
	"context"
	"fmt"
	"math"
	"sync"
	"time"
)

// BlockDetector provides block-level duplicate detection capabilities
type BlockDetector struct {
	index          BlockIndex
	fingerprinter  BlockFingerprinter
	matcher        *SimilarityMatcher
	config         DetectionConfig
	mu             sync.RWMutex
	stats          DetectionStats
}

// DetectionConfig contains configuration for block duplicate detection
type DetectionConfig struct {
	SimilarityThreshold float64
	MinBlockSize        int64
	MaxBlockSize        int64
	EnableSimilarity    bool
	EnableExactMatch    bool
	SamplingRate        float64 // For large datasets, sample this percentage of blocks
	MaxCandidates       int     // Maximum number of candidates to consider
}

// DetectionStats contains statistics for duplicate detection
type DetectionStats struct {
	TotalBlocks      int64
	DuplicateBlocks  int64
	UniqueBlocks     int64
	SimilarityChecks int64
	ExactMatches     int64
	SimilarMatches   int64
	ProcessingTime   time.Duration
	LastUpdate       time.Time
}

// DuplicateCandidate represents a potential duplicate block
type DuplicateCandidate struct {
	Hash         string
	Similarity   float64
	Size         int64
	Confidence   float64
	LastAccess   time.Time
}

// NewBlockDetector creates a new block duplicate detector
func NewBlockDetector(index BlockIndex, fingerprinter BlockFingerprinter, config DetectionConfig) (*BlockDetector, error) {
	if index == nil {
		return nil, fmt.Errorf("block index cannot be nil")
	}
	if fingerprinter == nil {
		return nil, fmt.Errorf("fingerprinter cannot be nil")
	}

	matcher := NewSimilarityMatcher(config.SimilarityThreshold, fingerprinter)

	return &BlockDetector{
		index:         index,
		fingerprinter: fingerprinter,
		matcher:       matcher,
		config:        config,
		stats: DetectionStats{
			LastUpdate: time.Now(),
		},
	}, nil
}

// DetectDuplicates detects duplicate blocks in the given data
func (d *BlockDetector) DetectDuplicates(ctx context.Context, data []byte) ([]DuplicateCandidate, error) {
	startTime := time.Now()
	defer func() {
		d.mu.Lock()
		d.stats.ProcessingTime += time.Since(startTime)
		d.stats.LastUpdate = time.Now()
		d.mu.Unlock()
	}()

	if len(data) == 0 {
		return nil, fmt.Errorf("empty data provided")
	}

	// Calculate fingerprint for the input data
	fingerprint := d.fingerprinter.CalculateFingerprint(data)

	// Find similar blocks
	candidates, err := d.findSimilarBlocks(ctx, fingerprint, data)
	if err != nil {
		return nil, fmt.Errorf("failed to find similar blocks: %w", err)
	}

	// Filter and rank candidates
	rankedCandidates := d.rankCandidates(candidates, data)

	d.updateStats(len(candidates))

	return rankedCandidates, nil
}

// findSimilarBlocks finds blocks similar to the given data
func (d *BlockDetector) findSimilarBlocks(ctx context.Context, fingerprint string, data []byte) ([]DuplicateCandidate, error) {
	var candidates []DuplicateCandidate

	// First, try exact match
	if d.config.EnableExactMatch {
		if block, err := d.index.GetBlock(fingerprint); err == nil {
			candidates = append(candidates, DuplicateCandidate{
				Hash:       fingerprint,
				Similarity: 1.0,
				Size:       block.Size,
				Confidence: 1.0,
				LastAccess: time.Unix(block.LastAccess, 0),
			})
		}
	}

	// If no exact match or similarity detection is enabled, find similar blocks
	if len(candidates) == 0 && d.config.EnableSimilarity {
		similarBlocks, err := d.index.FindSimilarBlocks(fingerprint, d.config.SimilarityThreshold)
		if err != nil {
			return nil, fmt.Errorf("failed to find similar blocks: %w", err)
		}

		for _, block := range similarBlocks {
			similarity := d.fingerprinter.CalculateSimilarity(fingerprint, block.Hash)
			if similarity >= d.config.SimilarityThreshold {
				candidates = append(candidates, DuplicateCandidate{
					Hash:       block.Hash,
					Similarity: similarity,
					Size:       block.Size,
					Confidence: d.calculateConfidence(similarity, block),
					LastAccess: time.Unix(block.LastAccess, 0),
				})
			}
		}
	}

	return candidates, nil
}

// rankCandidates ranks duplicate candidates by relevance
func (d *BlockDetector) rankCandidates(candidates []DuplicateCandidate, data []byte) []DuplicateCandidate {
	if len(candidates) <= 1 {
		return candidates
	}

	// Sort by confidence (descending), then by similarity (descending), then by last access (descending)
	for i := 0; i < len(candidates)-1; i++ {
		for j := i + 1; j < len(candidates); j++ {
			if d.shouldRankHigher(candidates[j], candidates[i]) {
				candidates[i], candidates[j] = candidates[j], candidates[i]
			}
		}
	}

	// Limit to maximum candidates
	if len(candidates) > d.config.MaxCandidates {
		candidates = candidates[:d.config.MaxCandidates]
	}

	return candidates
}

// shouldRankHigher determines if candidate a should rank higher than candidate b
func (d *BlockDetector) shouldRankHigher(a, b DuplicateCandidate) bool {
	// Higher confidence first
	if a.Confidence != b.Confidence {
		return a.Confidence > b.Confidence
	}

	// Higher similarity first
	if a.Similarity != b.Similarity {
		return a.Similarity > b.Similarity
	}

	// More recent access first
	return a.LastAccess.After(b.LastAccess)
}

// calculateConfidence calculates confidence score for a duplicate candidate
func (d *BlockDetector) calculateConfidence(similarity float64, block BlockMetadata) float64 {
	// Base confidence from similarity
	confidence := similarity

	// Adjust based on block size (larger blocks are more reliable)
	sizeFactor := 1.0
	if block.Size >= d.config.MinBlockSize {
		sizeFactor = math.Min(1.2, float64(block.Size)/float64(d.config.MinBlockSize))
	}
	confidence *= sizeFactor

	// Adjust based on reference count (more references = more reliable)
	if block.ReferenceCount > 0 {
		refFactor := math.Min(1.1, 1.0+float64(block.ReferenceCount)*0.01)
		confidence *= refFactor
	}

	// Adjust based on age (newer blocks are preferred)
	age := time.Since(time.Unix(block.CreatedAt, 0))
	if age < 24*time.Hour {
		confidence *= 1.05
	} else if age > 7*24*time.Hour {
		confidence *= 0.95
	}

	return math.Min(1.0, confidence)
}

// updateStats updates detection statistics
func (d *BlockDetector) updateStats(candidateCount int) {
	d.mu.Lock()
	defer d.mu.Unlock()

	d.stats.SimilarityChecks++
	if candidateCount > 0 {
		d.stats.DuplicateBlocks++
		if d.config.EnableExactMatch {
			d.stats.ExactMatches++
		}
		if d.config.EnableSimilarity {
			d.stats.SimilarMatches++
		}
	}
}

// BatchDetectDuplicates detects duplicates for multiple blocks
func (d *BlockDetector) BatchDetectDuplicates(ctx context.Context, blocks [][]byte) ([][]DuplicateCandidate, error) {
	results := make([][]DuplicateCandidate, len(blocks))

	for i, block := range blocks {
		candidates, err := d.DetectDuplicates(ctx, block)
		if err != nil {
			return nil, fmt.Errorf("failed to detect duplicates for block %d: %w", i, err)
		}
		results[i] = candidates
	}

	return results, nil
}

// GetDetectionStats returns current detection statistics
func (d *BlockDetector) GetDetectionStats() DetectionStats {
	d.mu.RLock()
	defer d.mu.RUnlock()

	return d.stats
}

// ResetStats resets detection statistics
func (d *BlockDetector) ResetStats() {
	d.mu.Lock()
	defer d.mu.Unlock()

	d.stats = DetectionStats{
		LastUpdate: time.Now(),
	}
}

// UpdateConfig updates detection configuration
func (d *BlockDetector) UpdateConfig(config DetectionConfig) error {
	d.mu.Lock()
	defer d.mu.Unlock()

	if config.SimilarityThreshold < 0 || config.SimilarityThreshold > 1 {
		return fmt.Errorf("invalid similarity threshold: %f", config.SimilarityThreshold)
	}

	if config.MinBlockSize < 0 {
		return fmt.Errorf("invalid minimum block size: %d", config.MinBlockSize)
	}

	if config.MaxBlockSize < config.MinBlockSize {
		return fmt.Errorf("max block size must be >= min block size")
	}

	d.config = config
	d.matcher.SetSimilarityThreshold(config.SimilarityThreshold)

	return nil
}

// OptimizeIndex optimizes the block index for better detection performance
func (d *BlockDetector) OptimizeIndex(ctx context.Context) error {
	// This could include operations like:
	// - Removing stale entries
	// - Rebuilding indexes
	// - Compressing data
	// - Updating statistics

	if optimizer, ok := d.index.(interface{ Optimize() error }); ok {
		return optimizer.Optimize()
	}

	return nil
}

// GetDuplicateRatio returns the current duplicate ratio
func (d *BlockDetector) GetDuplicateRatio() float64 {
	d.mu.RLock()
	defer d.mu.RUnlock()

	if d.stats.TotalBlocks == 0 {
		return 0.0
	}

	return float64(d.stats.DuplicateBlocks) / float64(d.stats.TotalBlocks)
}

// EstimateStorageSavings estimates potential storage savings from deduplication
func (d *BlockDetector) EstimateStorageSavings(ctx context.Context, blockSize int64) (int64, error) {
	d.mu.RLock()
	defer d.mu.RUnlock()

	if d.stats.DuplicateBlocks == 0 {
		return 0, nil
	}

	// Estimate savings based on duplicate ratio and average block size
	duplicateRatio := d.GetDuplicateRatio()
	estimatedSavings := int64(float64(blockSize) * float64(d.stats.DuplicateBlocks) * duplicateRatio)

	return estimatedSavings, nil
}